//
//  Macintosh.cpp
//  Clock Signal
//
//  Created by Thomas Harte on 03/05/2019.
//  Copyright © 2019 Thomas Harte. All rights reserved.
//

#include "Macintosh.hpp"

#include <array>

#include "DeferredAudio.hpp"
#include "DriveSpeedAccumulator.hpp"
#include "Keyboard.hpp"
#include "Video.hpp"

#include "Machines/MachineTypes.hpp"
#include "Activity/Source.hpp"
#include "Configurable/Configurable.hpp"

#include "Inputs/QuadratureMouse/QuadratureMouse.hpp"
#include "Outputs/Log.hpp"

#include "ClockReceiver/JustInTime.hpp"
#include "ClockReceiver/ClockingHintSource.hpp"
#include "Configurable/StandardOptions.hpp"

//#define LOG_TRACE

#include "Components/5380/ncr5380.hpp"
#include "Components/6522/6522.hpp"
#include "Components/8530/z8530.hpp"
#include "Components/AppleClock/AppleClock.hpp"
#include "Components/DiskII/IWM.hpp"
#include "Components/DiskII/MacintoshDoubleDensityDrive.hpp"

#include "Processors/68000/68000.hpp"

#include "Storage/MassStorage/SCSI/SCSI.hpp"
#include "Storage/MassStorage/SCSI/DirectAccessDevice.hpp"
#include "Storage/MassStorage/Encodings/MacintoshVolume.hpp"

#include "Analyser/Static/Macintosh/Target.hpp"

#include "Machines/Utility/MemoryPacker.hpp"
#include "Machines/Utility/MemoryFuzzer.hpp"

namespace {

constexpr int CLOCK_RATE = 7833600;
constexpr auto KEYBOARD_CLOCK_RATE = HalfCycles(CLOCK_RATE / 100000);
using Logger = Log::Logger<Log::Source::Macintosh>;

//	Former default PRAM:
//
//				0xa8, 0x00, 0x00, 0x00,
//				0xcc, 0x0a, 0xcc, 0x0a,
//				0x00, 0x00, 0x00, 0x00,
//				0x00, 0x02, 0x63, 0x00,
//				0x03, 0x88, 0x00, 0x4c

}

namespace Apple {
namespace Macintosh {

template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachine:
	public Machine,
	public MachineTypes::TimedMachine,
	public MachineTypes::ScanProducer,
	public MachineTypes::AudioProducer,
	public MachineTypes::MediaTarget,
	public MachineTypes::MouseMachine,
	public MachineTypes::MappedKeyboardMachine,
	public CPU::MC68000::BusHandler,
	public Zilog::SCC::z8530::Delegate,
	public Activity::Source,
	public Configurable::Device,
	public DriveSpeedAccumulator::Delegate,
	public ClockingHint::Observer {
public:
	using Target = Analyser::Static::Macintosh::Target;

	ConcreteMachine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
		MachineTypes::MappedKeyboardMachine({
			Inputs::Keyboard::Key::LeftShift, Inputs::Keyboard::Key::RightShift,
			Inputs::Keyboard::Key::LeftOption, Inputs::Keyboard::Key::RightOption,
			Inputs::Keyboard::Key::LeftMeta, Inputs::Keyboard::Key::RightMeta,
		}),
		mc68000_(*this),
		iwm_(CLOCK_RATE),
		video_(audio_, drive_speed_accumulator_),
		via_port_handler_(*this, clock_, keyboard_, audio_, iwm_, mouse_),
		via_(via_port_handler_),
		scsi_bus_(CLOCK_RATE * 2),
		scsi_(scsi_bus_, CLOCK_RATE * 2),
		hard_drive_(scsi_bus_, 6 /* SCSI ID */),
		drives_{
			{CLOCK_RATE, model >= Analyser::Static::Macintosh::Target::Model::Mac512ke},
			{CLOCK_RATE, model >= Analyser::Static::Macintosh::Target::Model::Mac512ke}
		},
		mouse_(1) {

		// Select a ROM name and determine the proper ROM and RAM sizes
		// based on the machine model.
		using Model = Analyser::Static::Macintosh::Target::Model;
		const std::string machine_name = "Macintosh";
		uint32_t ram_size, rom_size;
		ROM::Name rom_name;
		switch(model) {
			default:
			case Model::Mac128k:
				ram_size = 128*1024;
				rom_size = 64*1024;
				rom_name = ROM::Name::Macintosh128k;
			break;
			case Model::Mac512k:
				ram_size = 512*1024;
				rom_size = 64*1024;
				rom_name = ROM::Name::Macintosh512k;
			break;
			case Model::Mac512ke:
			case Model::MacPlus: {
				ram_size = ((model == Model::MacPlus) ? 4096 : 512)*1024;
				rom_size = 128*1024;
				rom_name = ROM::Name::MacintoshPlus;
			} break;
		}
		ram_mask_ = ram_size - 1;
		rom_mask_ = rom_size - 1;
		ram_.resize(ram_size);
		video_.set_ram(reinterpret_cast<uint16_t *>(ram_.data()), ram_mask_ >> 1);

		// Grab a copy of the ROM and convert it into big-endian data.
		ROM::Request request(rom_name);
		auto roms = rom_fetcher(request);
		if(!request.validate(roms)) {
			throw ROMMachine::Error::MissingROMs;
		}
		Memory::PackBigEndian16(roms.find(rom_name)->second, rom_);

		// Randomise memory contents.
		Memory::Fuzz(ram_);

		// Attach the drives to the IWM.
		iwm_->set_drive(0, &drives_[0]);
		iwm_->set_drive(1, &drives_[1]);

		// If they are 400kb drives, also attach them to the drive-speed accumulator.
		if(!drives_[0].is_800k() || !drives_[1].is_800k()) {
			drive_speed_accumulator_.set_delegate(this);
		}

		// Make sure interrupt changes from the SCC are observed.
		scc_.set_delegate(this);

		// Also watch for changes in clocking requirement from the SCSI chip.
		if constexpr (model == Analyser::Static::Macintosh::Target::Model::MacPlus) {
			scsi_bus_.set_clocking_hint_observer(this);
		}

		// The Mac runs at 7.8336mHz.
		set_clock_rate(double(CLOCK_RATE));
		audio_.speaker.set_input_rate(float(CLOCK_RATE) / 2.0f);

		// Insert any supplied media.
		insert_media(target.media);

		// Set the immutables of the memory map.
		setup_memory_map();
	}

	~ConcreteMachine() {
		audio_.queue.lock_flush();
	}

	void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
		video_.set_scan_target(scan_target);
	}

	Outputs::Display::ScanStatus get_scaled_scan_status() const final {
		return video_.get_scaled_scan_status();
	}

	Outputs::Speaker::Speaker *get_speaker() final {
		return &audio_.speaker;
	}

	void run_for(const Cycles cycles) final {
		mc68000_.run_for(cycles);
	}

	template <typename Microcycle> HalfCycles perform_bus_operation(const Microcycle &cycle, int) {
		// Advance time.
		advance_time(cycle.length);

		// A null cycle leaves nothing else to do.
		if(!(cycle.operation & (CPU::MC68000::Operation::NewAddress | CPU::MC68000::Operation::SameAddress))) return HalfCycles(0);

		// Grab the address.
		auto address = cycle.host_endian_byte_address();

		// Everything above E0 0000 is signalled as being on the peripheral bus.
		//
		// This will also act to autovector interrupts, since an interrupt acknowledge
		// cycle posts an address with all the higher-order bits set, and VPA doubles
		// as the input to request an autovector.
		mc68000_.set_is_peripheral_address(address >= 0xe0'0000);

		// All code below deals only with reads and writes — cycles in which a
		// data select is active. So quit now if this is not the active part of
		// a read or write.
		//
		// The 68000 uses 6800-style autovectored interrupts, so the mere act of
		// having set VPA above deals with those given that the generated address
		// for interrupt acknowledge cycles always has all bits set except the
		// lowest explicit address lines.
		if(
			!cycle.data_select_active() ||
			(cycle.operation & CPU::MC68000::Operation::InterruptAcknowledge)
		) return HalfCycles(0);

		// Grab the word-precision address being accessed.
		uint8_t *memory_base = nullptr;
		HalfCycles delay;
		switch(memory_map_[address >> 17]) {
			default:
				assert(false);
				[[fallthrough]];

			case BusDevice::Unassigned:
				fill_unmapped(cycle);
			return delay;

			case BusDevice::VIA: {
				if(*cycle.address & 1) {
					fill_unmapped(cycle);
				} else {
					const int register_address = address >> 9;

					// VIA accesses are via address 0xefe1fe + register*512,
					// which at word precision is 0x77f0ff + register*256.
					if(cycle.operation & CPU::MC68000::Operation::Read) {
						cycle.set_value8_high(via_.read(register_address));
					} else {
						via_.write(register_address, cycle.value8_high());
					}
				}
			} return delay;

			case BusDevice::PhaseRead: {
				if(cycle.operation & CPU::MC68000::Operation::Read) {
					cycle.set_value8_low(phase_ & 7);
				}
			} return delay;

			case BusDevice::IWM: {
				if(*cycle.address & 1) {
					const int register_address = address >> 9;

					// The IWM; this is a purely polled device, so can be run on demand.
					if(cycle.operation & CPU::MC68000::Operation::Read) {
						cycle.set_value8_low(iwm_->read(register_address));
					} else {
						iwm_->write(register_address, cycle.value8_low());
					}
				} else {
					fill_unmapped(cycle);
				}
			} return delay;

			case BusDevice::SCSI: {
				const int register_address = address >> 4;
				const bool dma_acknowledge = address & 0x200;

				// Even accesses = read; odd = write.
				if(*cycle.address & 1) {
					// Odd access => this is a write. Data will be in the upper byte.
					if(cycle.operation & CPU::MC68000::Operation::Read) {
						scsi_.write(register_address, 0xff, dma_acknowledge);
					} else {
						scsi_.write(register_address, cycle.value8_high());
					}
				} else {
					// Even access => this is a read.
					if(cycle.operation & CPU::MC68000::Operation::Read) {
						cycle.set_value8_high(scsi_.read(register_address, dma_acknowledge));
					}
				}
			} return delay;

			case BusDevice::SCCReadResetPhase: {
				// Any word access here adjusts phase.
				if(cycle.operation & CPU::MC68000::Operation::SelectWord) {
					adjust_phase();
				} else {
					// A0 = 1 => reset; A0 = 0 => read.
					if(*cycle.address & 1) {
						scc_.reset();

						if(cycle.operation & CPU::MC68000::Operation::Read) {
							cycle.set_value16(0xffff);
						}
					} else {
						const auto read = scc_.read(int(address >> 1));
						if(cycle.operation & CPU::MC68000::Operation::Read) {
							cycle.set_value8_high(read);
						}
					}
				}
			} return delay;

			case BusDevice::SCCWrite: {
				// Any word access here adjusts phase.
				if(cycle.operation & CPU::MC68000::Operation::SelectWord) {
					adjust_phase();
				} else {
					// This is definitely a byte access; either it's to an odd address, in which
					// case it will reach the SCC, or it isn't, in which case it won't.
					if(*cycle.address & 1) {
						if(cycle.operation & CPU::MC68000::Operation::Read) {
							scc_.write(int(address >> 1), 0xff);
							cycle.value->b = 0xff;
						} else {
							scc_.write(int(address >> 1), cycle.value->b);
						}
					} else {
						fill_unmapped(cycle);
					}
				}
			} return delay;

			case BusDevice::RAM: {
				// This is coupled with the Macintosh implementation of video; the magic
				// constant should probably be factored into the Video class.
				// It embodies knowledge of the fact that video (and audio) will always
				// be fetched from the final $d900 bytes of memory.
				// (And that ram_mask_ = ram size - 1).
				if(address > ram_mask_ - 0xd900)
					update_video();

				memory_base = ram_.data();
				address &= ram_mask_;

				// Apply a delay due to video contention if applicable; scheme applied:
				// only every other access slot is available during the period of video
				// output. I believe this to be correct for the 128k, 512k and Plus.
				// More research to do on other models.
				if(video_is_outputting() && ram_subcycle_ < 8) {
					delay = HalfCycles(8 - ram_subcycle_);
					advance_time(delay);
				}
			} break;

			case BusDevice::ROM: {
				if(!(cycle.operation & CPU::MC68000::Operation::Read)) return delay;
				memory_base = rom_;
				address &= rom_mask_;
			} break;
		}

		// If control has fallen through to here, the access is either a read from ROM, or a read or write to RAM.
		// Potential writes to ROM and all hardware accesses have already been weeded out.
		cycle.apply(&memory_base[address]);

		return delay;
	}

	void flush_output(int) {
		// Flush the video before the audio queue; in a Mac the
		// video is responsible for providing part of the
		// audio signal, so the two aren't as distinct as in
		// most machines.
		update_video();

		// As above: flush audio after video.
		via_.flush();
		audio_.queue.perform();

		// This avoids deferring IWM costs indefinitely, until
		// they become artbitrarily large.
		iwm_.flush();
	}

	void set_rom_is_overlay(bool rom_is_overlay) {
		ROM_is_overlay_ = rom_is_overlay;

		using Model = Analyser::Static::Macintosh::Target::Model;
		switch(model) {
			case Model::Mac128k:
			case Model::Mac512k:
			case Model::Mac512ke:
				populate_memory_map(0, [rom_is_overlay] (std::function<void(int target, BusDevice device)> map_to) {
					// Addresses up to $80 0000 aren't affected by this bit.
					if(rom_is_overlay) {
						// Up to $60 0000 mirrors of the ROM alternate with unassigned areas every $10 0000 byes.
						for(int c = 0; c < 0x600000; c += 0x100000) {
							map_to(c + 0x100000, (c & 0x100000) ? BusDevice::Unassigned : BusDevice::ROM);
						}
						map_to(0x800000, BusDevice::RAM);
					} else {
						map_to(0x400000, BusDevice::RAM);
						map_to(0x500000, BusDevice::ROM);
						map_to(0x800000, BusDevice::Unassigned);
					}
				});
			break;

			case Model::MacPlus:
				populate_memory_map(0, [rom_is_overlay] (std::function<void(int target, BusDevice device)> map_to) {
					// Addresses up to $80 0000 aren't affected by this bit.
					if(rom_is_overlay) {
						for(int c = 0; c < 0x580000; c += 0x20000) {
							map_to(c + 0x20000, ((c & 0x100000) || (c & 0x20000)) ? BusDevice::Unassigned : BusDevice::ROM);
						}
						map_to(0x600000, BusDevice::SCSI);
						map_to(0x800000, BusDevice::RAM);
					} else {
						map_to(0x400000, BusDevice::RAM);
						for(int c = 0x400000; c < 0x580000; c += 0x20000) {
							map_to(c + 0x20000, ((c & 0x100000) || (c & 0x20000)) ? BusDevice::Unassigned : BusDevice::ROM);
						}
						map_to(0x600000, BusDevice::SCSI);
						map_to(0x800000, BusDevice::Unassigned);
					}
				});
			break;
		}
	}

	bool video_is_outputting() {
		return video_.is_outputting(time_since_video_update_);
	}

	void set_use_alternate_buffers(bool use_alternate_screen_buffer, bool use_alternate_audio_buffer) {
		update_video();
		video_.set_use_alternate_buffers(use_alternate_screen_buffer, use_alternate_audio_buffer);
	}

	bool insert_media(const Analyser::Static::Media &media) final {
		if(media.disks.empty() && media.mass_storage_devices.empty())
			return false;

		// TODO: shouldn't allow disks to be replaced like this, as the Mac
		// uses software eject. Will need to expand messaging ability of
		// insert_media.
		if(!media.disks.empty()) {
			if(drives_[0].has_disk())
				drives_[1].set_disk(media.disks[0]);
			else
				drives_[0].set_disk(media.disks[0]);
		}

		// TODO: allow this only at machine startup?
		if(!media.mass_storage_devices.empty()) {
			const auto volume = dynamic_cast<Storage::MassStorage::Encodings::Macintosh::Volume *>(media.mass_storage_devices.front().get());
			if(volume) {
				volume->set_drive_type(Storage::MassStorage::Encodings::Macintosh::DriveType::SCSI);
			}
			hard_drive_->set_storage(media.mass_storage_devices.front());
		}

		return true;
	}

	// MARK: Keyboard input.

	KeyboardMapper *get_keyboard_mapper() final {
		return &keyboard_mapper_;
	}

	void set_key_state(uint16_t key, bool is_pressed) final {
		keyboard_.enqueue_key_state(key, is_pressed);
	}

	// TODO: clear all keys.

	// MARK: Interrupt updates.

	void did_change_interrupt_status(Zilog::SCC::z8530 &, bool) final {
		update_interrupt_input();
	}

	void update_interrupt_input() {
		// Update interrupt input.
		// TODO: does this really cascade like this?
		if(scc_.get_interrupt_line()) {
			mc68000_.set_interrupt_level(2);
		} else if(via_.get_interrupt_line()) {
			mc68000_.set_interrupt_level(1);
		} else {
			mc68000_.set_interrupt_level(0);
		}
	}

	// MARK: - Activity Source
	void set_activity_observer(Activity::Observer *observer) final {
		iwm_->set_activity_observer(observer);

		if constexpr (model == Analyser::Static::Macintosh::Target::Model::MacPlus) {
			scsi_bus_.set_activity_observer(observer);
		}
	}

	// MARK: - Configuration options.
	std::unique_ptr<Reflection::Struct> get_options() const final {
		auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
		options->quick_boot = quickboot_;
		return options;
	}

	void set_options(const std::unique_ptr<Reflection::Struct> &str) final {
		// TODO: should this really be a runtime option?
		// It should probably be a construction option.

		const auto options = dynamic_cast<Options *>(str.get());
		quickboot_ = options->quick_boot;

		using Model = Analyser::Static::Macintosh::Target::Model;
		const bool is_plus_rom = model == Model::Mac512ke || model == Model::MacPlus;
		if(quickboot_ && is_plus_rom) {
			// Cf. Big Mess o' Wires' disassembly of the Mac Plus ROM, and the
			// test at $E00. TODO: adapt as(/if?) necessary for other Macs.
			ram_[0x02ae] = 0x40;
			ram_[0x02af] = 0x00;
			ram_[0x02b0] = 0x00;
			ram_[0x02b1] = 0x00;
		}
	}

private:
	bool quickboot_ = false;

	void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference) final {
		scsi_bus_is_clocked_ = scsi_bus_.preferred_clocking() != ClockingHint::Preference::None;
	}

	void drive_speed_accumulator_set_drive_speed(DriveSpeedAccumulator &, const float speed) final {
		iwm_.flush();
		drives_[0].set_rotation_speed(speed);
		drives_[1].set_rotation_speed(speed);
	}

	forceinline void adjust_phase() {
		++phase_;
	}

	template <typename Microcycle>
	forceinline void fill_unmapped(const Microcycle &cycle) {
		if(!(cycle.operation & CPU::MC68000::Operation::Read)) return;
		cycle.set_value16(0xffff);
	}

	/// Advances all non-CPU components by @c duration half cycles.
	forceinline void advance_time(HalfCycles duration) {
		time_since_video_update_ += duration;
		iwm_ += duration;
		ram_subcycle_ = (ram_subcycle_ + duration.as_integral()) & 15;

		// The VIA runs at one-tenth of the 68000's clock speed, in sync with the E clock.
		// See: Guide to the Macintosh Hardware Family p149 (PDF p188). Some extra division
		// may occur here in order to provide VSYNC at a proper moment.

		// Possibly route vsync.
		if(time_since_video_update_ < time_until_video_event_) {
			via_clock_ += duration;
			via_.run_for(via_clock_.divide(HalfCycles(10)));
		} else {
			auto via_time_base = time_since_video_update_ - duration;
			auto via_cycles_outstanding = duration;
			while(time_until_video_event_ < time_since_video_update_) {
				const auto via_cycles = time_until_video_event_ - via_time_base;
				via_time_base = HalfCycles(0);
				via_cycles_outstanding -= via_cycles;

				via_clock_ += via_cycles;
				via_.run_for(via_clock_.divide(HalfCycles(10)));

				video_.run_for(time_until_video_event_);
				time_since_video_update_ -= time_until_video_event_;
				time_until_video_event_ = video_.next_sequence_point();

				via_.template set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::One>(!video_.vsync());
			}

			via_clock_ += via_cycles_outstanding;
			via_.run_for(via_clock_.divide(HalfCycles(10)));
		}

		// The keyboard also has a clock, albeit a very slow one — 100,000 cycles/second.
		// Its clock and data lines are connected to the VIA.
		keyboard_clock_ += duration;
		if(keyboard_clock_ >= KEYBOARD_CLOCK_RATE) {
			const auto keyboard_ticks = keyboard_clock_.divide(KEYBOARD_CLOCK_RATE);
			keyboard_.run_for(keyboard_ticks);
			via_.template set_control_line_input<MOS::MOS6522::Port::B, MOS::MOS6522::Line::Two>(keyboard_.get_data());
			via_.template set_control_line_input<MOS::MOS6522::Port::B, MOS::MOS6522::Line::One>(keyboard_.get_clock());
		}

		// Feed mouse inputs within at most 1250 cycles of each other.
		if(mouse_.has_steps()) {
			time_since_mouse_update_ += duration;
			const auto mouse_ticks = time_since_mouse_update_.divide(HalfCycles(2500));
			if(mouse_ticks > HalfCycles(0)) {
				mouse_.prepare_step();
				scc_.set_dcd(0, mouse_.get_channel(1) & 1);
				scc_.set_dcd(1, mouse_.get_channel(0) & 1);
			}
		}

		// TODO: SCC should be clocked at a divide-by-two, if and when it actually has
		// anything connected.

		// Consider updating the real-time clock.
		real_time_clock_ += duration;
		auto ticks = real_time_clock_.divide_cycles(Cycles(CLOCK_RATE)).as_integral();
		while(ticks--) {
			clock_.update();
			// TODO: leave a delay between toggling the input rather than using this coupled hack.
			via_.template set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two>(true);
			via_.template set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two>(false);
		}

		// Update the SCSI if currently active.
		if constexpr (model == Analyser::Static::Macintosh::Target::Model::MacPlus) {
			if(scsi_bus_is_clocked_) scsi_bus_.run_for(duration);
		}
	}

	forceinline void update_video() {
		video_.run_for(time_since_video_update_.flush<HalfCycles>());
		time_until_video_event_ = video_.next_sequence_point();
	}

	Inputs::Mouse &get_mouse() final {
		return mouse_;
	}

	using IWMActor = JustInTimeActor<IWM>;

	class VIAPortHandler: public MOS::MOS6522::PortHandler {
	public:
		VIAPortHandler(ConcreteMachine &machine, Apple::Clock::SerialClock &clock, Keyboard &keyboard, DeferredAudio &audio, IWMActor &iwm, Inputs::QuadratureMouse &mouse) :
			machine_(machine), clock_(clock), keyboard_(keyboard), audio_(audio), iwm_(iwm), mouse_(mouse) {}

		using Port = MOS::MOS6522::Port;
		using Line = MOS::MOS6522::Line;

		template <Port port> void set_port_output(const uint8_t value, uint8_t) {
			/*
				Peripheral lines: keyboard data, interrupt configuration.
				(See p176 [/215])
			*/
			switch(port) {
				case Port::A:
					/*
						Port A:
							b7:	[input] SCC wait/request (/W/REQA and /W/REQB wired together for a logical OR)
							b6:	0 = alternate screen buffer, 1 = main screen buffer
							b5:	floppy disk SEL state control (upper/lower head "among other things")
							b4:	1 = use ROM overlay memory map, 0 = use ordinary memory map
							b3:	0 = use alternate sound buffer, 1 = use ordinary sound buffer
							b2–b0:	audio output volume
					*/
					iwm_->set_select(!!(value & 0x20));

					machine_.set_use_alternate_buffers(!(value & 0x40), !(value&0x08));
					machine_.set_rom_is_overlay(!!(value & 0x10));

					audio_.flush();
					audio_.audio.set_volume(value & 7);
				break;

				case Port::B:
					/*
						Port B:
							b7:	0 = sound enabled, 1 = sound disabled
							b6:	[input] 0 = video beam in visible portion of line, 1 = outside
							b5:	[input] mouse y2
							b4:	[input] mouse x2
							b3:	[input] 0 = mouse button down, 1 = up
							b2:	0 = real-time clock enabled, 1 = disabled
							b1:	clock's data-clock line
							b0:	clock's serial data line
					*/
					if(value & 0x4) clock_.abort();
					else clock_.set_input(!!(value & 0x2), !!(value & 0x1));

					audio_.flush();
					audio_.audio.set_enabled(!(value & 0x80));
				break;
			}
		}

		template <Port port> uint8_t get_port_input() const {
			switch(port) {
				case Port::A:
//							printf("6522 r A\n");
				return 0x00;	// TODO: b7 = SCC wait/request

				case Port::B:
				return uint8_t(
					((mouse_.get_button_mask() & 1) ? 0x00 : 0x08) |
					((mouse_.get_channel(0) & 2) << 3) |
					((mouse_.get_channel(1) & 2) << 4) |
					(clock_.get_data() ? 0x02 : 0x00) |
					(machine_.video_is_outputting() ? 0x00 : 0x40)
				);
			}

			// Should be unreachable.
			return 0xff;
		}

		template <Port port, Line line> void set_control_line_output(const bool value) {
			/*
				Keyboard wiring (I believe):
				CB2 = data		(input/output)
				CB1 = clock		(input)

				CA2 is used for receiving RTC interrupts.
				CA1 is used for receiving vsync.
			*/
			if(port == Port::B && line == Line::Two) {
				keyboard_.set_input(value);
			}
			else Logger::error().append("Unhandled 6522 control line output: %c%d", port ? 'B' : 'A', int(line));
		}

		void run_for(HalfCycles duration) {
			// The 6522 enjoys a divide-by-ten, so multiply back up here to make the
			// divided-by-two clock the audio works on.
			audio_.time_since_update += HalfCycles(duration.as_integral() * 5);
		}

		void flush() {
			audio_.flush();
		}

		void set_interrupt_status(bool) {
			machine_.update_interrupt_input();
		}

	private:
		ConcreteMachine &machine_;
		Apple::Clock::SerialClock &clock_;
		Keyboard &keyboard_;
		DeferredAudio &audio_;
		IWMActor &iwm_;
		Inputs::QuadratureMouse &mouse_;
	};

	CPU::MC68000::Processor<ConcreteMachine, true, true> mc68000_;

	DriveSpeedAccumulator drive_speed_accumulator_;
	IWMActor iwm_;

	DeferredAudio audio_;
	Video video_;

	Apple::Clock::SerialClock clock_;
	Keyboard keyboard_;

	VIAPortHandler via_port_handler_;
	MOS::MOS6522::MOS6522<VIAPortHandler> via_;

	Zilog::SCC::z8530 scc_;
	SCSI::Bus scsi_bus_;
	NCR::NCR5380::NCR5380 scsi_;
	SCSI::Target::Target<SCSI::DirectAccessDevice> hard_drive_;
	bool scsi_bus_is_clocked_ = false;

	HalfCycles via_clock_;
	HalfCycles real_time_clock_;
	HalfCycles keyboard_clock_;
	HalfCycles time_since_video_update_;
	HalfCycles time_until_video_event_;
	HalfCycles time_since_mouse_update_;

	bool ROM_is_overlay_ = true;
	int phase_ = 1;
	int ram_subcycle_ = 0;

	DoubleDensityDrive drives_[2];
	Inputs::QuadratureMouse mouse_;

	Apple::Macintosh::KeyboardMapper keyboard_mapper_;

	enum class BusDevice {
		RAM, ROM, VIA, IWM, SCCWrite, SCCReadResetPhase, SCSI, PhaseRead, Unassigned
	};

	/// Divides the 24-bit address space up into $20000 (i.e. 128kb) segments, recording
	/// which device is current mapped in each area. Keeping it in a table is a bit faster
	/// than the multi-level address inspection that is otherwise required, as well as
	/// simplifying slightly the handling of different models.
	///
	/// So: index with the top 7 bits of the 24-bit address.
	BusDevice memory_map_[128];

	void setup_memory_map() {
		// Apply the power-up memory map, i.e. assume that ROM_is_overlay_ = true;
		// start by calling into set_rom_is_overlay to seed everything up to $800000.
		set_rom_is_overlay(true);

		populate_memory_map(0x800000, [] (std::function<void(int target, BusDevice device)> map_to) {
			map_to(0x900000, BusDevice::Unassigned);
			map_to(0xa00000, BusDevice::SCCReadResetPhase);
			map_to(0xb00000, BusDevice::Unassigned);
			map_to(0xc00000, BusDevice::SCCWrite);
			map_to(0xd00000, BusDevice::Unassigned);
			map_to(0xe00000, BusDevice::IWM);
			map_to(0xe80000, BusDevice::Unassigned);
			map_to(0xf00000, BusDevice::VIA);
			map_to(0xf80000, BusDevice::PhaseRead);
			map_to(0x1000000, BusDevice::Unassigned);
		});
	}

	void populate_memory_map(int start_address, std::function<void(std::function<void(int, BusDevice)>)> populator) {
		// Define semantics for below; map_to will write from the current cursor position
		// to the supplied 24-bit address, setting a particular mapped device.
		int segment = start_address >> 17;
		auto map_to = [&segment, this](int address, BusDevice device) {
			for(; segment < address >> 17; ++segment) {
				this->memory_map_[segment] = device;
			}
		};

		populator(map_to);
	}

	uint32_t ram_mask_ = 0;
	uint32_t rom_mask_ = 0;
	uint8_t rom_[128*1024];
	std::vector<uint8_t> ram_;
};

}
}

using namespace Apple::Macintosh;

std::unique_ptr<Machine> Machine::Macintosh(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
	auto *const mac_target = dynamic_cast<const Analyser::Static::Macintosh::Target *>(target);

	using Model = Analyser::Static::Macintosh::Target::Model;
	switch(mac_target->model) {
		default:
		case Model::Mac128k:	return std::make_unique<ConcreteMachine<Model::Mac128k>>(*mac_target, rom_fetcher);
		case Model::Mac512k:	return std::make_unique<ConcreteMachine<Model::Mac512k>>(*mac_target, rom_fetcher);
		case Model::Mac512ke:	return std::make_unique<ConcreteMachine<Model::Mac512ke>>(*mac_target, rom_fetcher);
		case Model::MacPlus:	return std::make_unique<ConcreteMachine<Model::MacPlus>>(*mac_target, rom_fetcher);
	}
}
